﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Diagnostics;
using System.Collections;

namespace Master
{
    /**
     * This class represents a 'Road'. 
     * A road contains one or several lanes with the same orientation.
     * 
     * In WPF-terms, the road is a GRID.
     * 
     * @Author: T.T. Dusseldorp
     * @Changes made by:
     *      - A.J. Janssen
     * */
    public class Road : Grid
    {
        /******************
         * INITIALIZATION *
         ******************/

        public Road()
        {
            this.Initialized += Road_Initialized; //hookup the initialized event to the road_initialized method
        }

        void Road_Initialized(object sender, EventArgs e) //gets called when the road is initialized
        {
            createNpOutgoing(); //place NpOutgoingPoint
        }

        /********************
         * PUBLIC FUNCTIONS *
         ********************/

        /**
         * Method to retrieve all lanes that fall within this road
         * */
        public Lane[] getLanes()
        {
            List<Lane> laneList = new List<Lane>();
            foreach (UIElement child in Children) if (child is Lane)
                laneList.Add((Lane) child);
            return laneList.ToArray();
        }

        /**
         * Get the lane number relative to the given direction.
         * This method will grab all lanes with the given direction, sort them and then return the index (+1) of the specified lane
         * */
        public int getLaneNumberByDirection(Lane lane, Direction direction)
        {
            Lane[] lanes = getLanesWithAllowedDirection(direction);
            if (lanes.Count() == 1) return 1;
            if (!lanes.Contains(lane)) throw new InvalidOperationException("That lane does not allow the specified direction");
            int orientation = lane.Orientation;
            if ((orientation == 0 && direction == Direction.LEFT) || (orientation == 180 && direction == Direction.RIGHT))
                Array.Sort(lanes, new CompareLaneBySmallestX());
            else if ((orientation == 0 && direction == Direction.RIGHT) || (orientation == 180 && direction == Direction.LEFT))
                Array.Sort(lanes, new CompareLaneByHighestX());
            else if ((orientation == 90 && direction == Direction.LEFT) || (orientation == 270 && direction == Direction.RIGHT))
                Array.Sort(lanes, new CompareLaneBySmallestY());
            else Array.Sort(lanes, new CompareLaneByHighestY());
            return Array.IndexOf(lanes, lane) + 1;
        }

        /**
         * Method to retrieve all lanes with the specified direction
         * */
        public Lane[] getLanesWithAllowedDirection(Direction direction)
        {
            List<Lane> resultList = Lane.getLanesWithAllowedDirection(getLanes(), direction);
            return resultList.ToArray();
        }

        /**
         * Method to retrieve whatever lanes allows the given direction and is closest to the specified lane
         * */
        public Lane getClosestLaneWithDirection(Lane currentLane, Direction direction)
        {
            Lane[] lanes = getLanesWithAllowedDirection(direction);
            if (lanes.Length == 0)
                throw new InvalidOperationException("No lanes with this direction exist");
            //check if any of these lanes is next to current lane (if true --> return lane)
            foreach (Lane lane in lanes)
                if (Lane.laneIsNextToLane(currentLane, lane))
                    return lane;
            //find closest lane
            Lane closestLane = (currentLane.Orientation == 0 || currentLane.Orientation == 180) ?
                getLaneWithSmallestXDifference(currentLane, lanes) : getLaneWithSmallestYDifference(currentLane, lanes);
            return getLaneInDirectionOfWantedLane(currentLane, closestLane);
        }

        /**
         * Method to retrieve whatever lane has the smallest x-difference compared the specified lane
         * */
        private Lane getLaneWithSmallestXDifference(Lane currentLane, Lane[] lanes)
        {
            double smallestDifference = 0; Lane closestLane = null;
            foreach (Lane lane in lanes)
            {
                double differenceWithCurrentLane = Toolkit.getXDistanceBetweenElements(currentLane, lane);
                if (closestLane == null || differenceWithCurrentLane < smallestDifference)
                {
                    smallestDifference = differenceWithCurrentLane;
                    closestLane = lane;
                }
            }
            return closestLane;
        }

        /**
         * Method to retrieve whatever lane has the smallest y-difference compared the specified lane
         * */
        private Lane getLaneWithSmallestYDifference(Lane currentLane, Lane[] lanes)
        {
            double smallestDifference = 0; Lane closestLane = null;
            foreach (Lane lane in lanes)
            {
                double differenceWithCurrentLane = Toolkit.getYDistanceBetweenElements(currentLane, lane);
                if (closestLane == null || differenceWithCurrentLane < smallestDifference)
                {
                    smallestDifference = differenceWithCurrentLane;
                    closestLane = lane;
                }
            }
            return closestLane;
        }

        //method to find the lane a car needs to switch to in order to get closer to the wantedLane
        private Lane getLaneInDirectionOfWantedLane(Lane currentLane, Lane wantedLane)
        {
            int laneOrientation = currentLane.Orientation;
            Point currentLanePoint = Toolkit.getCenterPoint(currentLane);
            if (laneOrientation == 0 || laneOrientation == 180)
            {
                if (currentLanePoint.X > Toolkit.getCenterPoint(wantedLane).X)
                    return getLaneAtX(currentLanePoint.X - currentLane.ActualWidth);
                else
                    return getLaneAtX(currentLanePoint.X + currentLane.ActualWidth);
            }
            else
            {
                if (currentLanePoint.Y > Toolkit.getCenterPoint(wantedLane).Y)
                    return getLaneAtY(currentLanePoint.Y - currentLane.ActualHeight);
                else
                    return getLaneAtY(currentLanePoint.Y + currentLane.ActualHeight);
            }
        }

        /**
         * Method to get the utmost left lane from the given array of lanes
         * */
        public static Lane getLeftLaneFromLanes(Lane[] lanes)
        {
            int orientation = lanes[0].Orientation;
            switch (orientation)
            {
                case 0: return getXLaneWithSmallestX(lanes, 1);
                case 90: return getXLaneWithSmallestY(lanes, 1);
                case 180: return getXLaneWithHighestX(lanes, 1);
                case 270: return getXLaneWithHighestY(lanes, 1);
                default: throw new InvalidOperationException("Lane orientation is invalid");
            }
        }

        /**
         * Method to get the Xth left lane from the given array of lanes, where x = number
         * */
        public static Lane getLeftLaneFromLanes(Lane[] lanes, int number)
        {
            int orientation = lanes[0].Orientation;
            switch (orientation)
            {
                case 0: return getXLaneWithSmallestX(lanes, number);
                case 90: return getXLaneWithSmallestY(lanes, number);
                case 180: return getXLaneWithHighestX(lanes, number);
                case 270: return getXLaneWithHighestY(lanes, number);
                default: throw new InvalidOperationException("Lane orientation is invalid");
            }
        }

        /**
         * Method to get the utmost right lane from the given array of lanes
         * */
        public static Lane getRightLaneFromLanes(Lane[] lanes)
        {
            int orientation = lanes[0].Orientation;
            switch (orientation)
            {
                case 0: return getXLaneWithHighestX(lanes, 1);
                case 90: return getXLaneWithHighestY(lanes, 1);
                case 180: return getXLaneWithSmallestX(lanes, 1);
                case 270: return getXLaneWithSmallestY(lanes, 1);
                default: throw new InvalidOperationException("Lane orientation is invalid");
            }
        }

        /**
         * Method to get the Xth right lane from the given array of lanes, where x = number
         * */
        public static Lane getRightLaneFromLanes(Lane[] lanes, int number)
        {
            int orientation = lanes[0].Orientation;
            switch (orientation)
            {
                case 0: return getXLaneWithHighestX(lanes, number);
                case 90: return getXLaneWithHighestY(lanes, number);
                case 180: return getXLaneWithSmallestX(lanes, number);
                case 270: return getXLaneWithSmallestY(lanes, number);
                default: throw new InvalidOperationException("Lane orientation is invalid");
            }
        }

        /**
         * Methods to retrieve the Xth lane with the highest/smallest X or Y value, where x = number
         * */
        public static Lane getXLaneWithHighestX(Lane[] lanes, int number) { return getXLaneFromLanes(lanes, new CompareLaneByHighestX(), number); }
        public static Lane getXLaneWithSmallestX(Lane[] lanes, int number) { return getXLaneFromLanes(lanes, new CompareLaneBySmallestX(), number); }
        public static Lane getXLaneWithHighestY(Lane[] lanes, int number) { return getXLaneFromLanes(lanes, new CompareLaneByHighestY(), number); }
        public static Lane getXLaneWithSmallestY(Lane[] lanes, int number) { return getXLaneFromLanes(lanes, new CompareLaneBySmallestY(), number); }

        private static Lane getXLaneFromLanes(Lane[] lanes, IComparer comparer, int number) //method to get the Xth lane from the lanes-array sorted by the speficied comparer
        {
            number--; if (number < 0) number = 0;
            Array.Sort(lanes, comparer);
            int lastIndex = lanes.Count() - 1;
            if (number > lastIndex) return lanes[lastIndex];
            return lanes[number];
        }

        /**
         * Method to retrieve whatever lane is located at the specified X-coordinate, returns null if no such lane is found
         * */
        public Lane getLaneAtX(double x)
        {
            foreach (Lane lane in getLanes())
                if (Toolkit.elementIsAtX(lane, x))
                    return lane;
            return null;
        }

        /**
         * Method to retrieve whatever lane is located at the specified Y-coordinate, returns null if no such lane is found
         * */
        public Lane getLaneAtY(double y)
        {
            foreach (Lane lane in getLanes())
                if (Toolkit.elementIsAtY(lane, y))
                    return lane;
            return null;
        }

        /**
         * Method to create a NpOutgoingPoint (if needed) and locate it at the edge of the map
         * */
        private void createNpOutgoing()
        {
            NpOutgoing npOutgoing = null;
            Lane cLane = (Lane)this.Children[0];
            double x = Canvas.GetLeft(this);
            double y = Canvas.GetTop(this); 
            LinearGradientBrush myVerticalGradient = new LinearGradientBrush();
            switch (cLane.Orientation)
            {
                case 0:
                    if (y == 0)
                    {
                        npOutgoing = new NpOutgoing();
                        npOutgoing.Width = this.Width;
                        npOutgoing.Height = 300;
                        myVerticalGradient.StartPoint = new Point(0.5, 1);
                        myVerticalGradient.EndPoint = new Point(0.5, 0);
                        Canvas.SetTop(npOutgoing, y);
                        Canvas.SetLeft(npOutgoing, x);
                    }
                    break;
                case 90:
                    if ((x + this.Width) == Map.INSTANCE.Width)
                    {
                        myVerticalGradient.StartPoint = new Point(0, 0.5);
                        myVerticalGradient.EndPoint = new Point(1, 0.5);
                        npOutgoing = new NpOutgoing();
                        npOutgoing.Height = this.Height;
                        npOutgoing.Width = 300;
                        Canvas.SetTop(npOutgoing, y);
                        Canvas.SetLeft(npOutgoing, x + this.Width - 300);
                    }
                    break;
                case 180:
                    if ((y + this.Height) == Map.INSTANCE.Height)
                    {
                        myVerticalGradient.StartPoint = new Point(0.5, 0);
                        myVerticalGradient.EndPoint = new Point(0.5, 1);
                        npOutgoing = new NpOutgoing();
                        npOutgoing.Width = this.Width;
                        npOutgoing.Height = 300;
                        Canvas.SetTop(npOutgoing, y + this.Height - 300);
                        Canvas.SetLeft(npOutgoing, x);
                    }
                    break;
                case 270:
                    if (x == 0)
                    {
                        myVerticalGradient.StartPoint = new Point(1, 0.5);
                        myVerticalGradient.EndPoint = new Point(0, 0.5);
                        npOutgoing = new NpOutgoing();
                        npOutgoing.Height = this.Height;
                        npOutgoing.Width = 300;
                        Canvas.SetTop(npOutgoing, y);
                        Canvas.SetLeft(npOutgoing, x);
                    }
                    break;
            }
            if (npOutgoing != null)
            {
                myVerticalGradient.GradientStops.Add(
                    new GradientStop(Colors.Gray, 0.6));
                myVerticalGradient.GradientStops.Add(
                    new GradientStop(Colors.Red, 0.99));

                // Use the brush to paint the rectangle.
                npOutgoing.Background = myVerticalGradient;
                npOutgoing.Orientation = cLane.Orientation;
                Canvas.SetZIndex(npOutgoing, 2);
                Map.INSTANCE.Children.Add(npOutgoing);
            }
        }

        /**
         * Comparer used to sort lanes by highest X-coordinate
         * */
        private class CompareLaneByHighestX : IComparer
        {
            int IComparer.Compare(Object x, Object y)
            {
                if (!(x is Lane) || !(y is Lane)) throw new InvalidOperationException("Can only compare two lanes");
                Lane firstLane = (Lane)x, secondLane = (Lane)y;
                return (int)(Toolkit.getPointRelativeToMap(secondLane).X - Toolkit.getPointRelativeToMap(firstLane).X);
            }
        }

        /**
         * Comparer used to sort lanes by smallest X-coordinate
         * */
        private class CompareLaneBySmallestX : IComparer
        {
            int IComparer.Compare(Object x, Object y)
            {
                if (!(x is Lane) || !(y is Lane)) throw new InvalidOperationException("Can only compare two lanes");
                Lane firstLane = (Lane)x, secondLane = (Lane)y;
                return (int)(Toolkit.getPointRelativeToMap(firstLane).X - Toolkit.getPointRelativeToMap(secondLane).X);
            }
        }

        /**
         * Comparer used to sort lanes by highest Y-coordinate
         * */
        private class CompareLaneByHighestY : IComparer
        {
            int IComparer.Compare(Object x, Object y)
            {
                if (!(x is Lane) || !(y is Lane)) throw new InvalidOperationException("Can only compare two lanes");
                Lane firstLane = (Lane)x, secondLane = (Lane)y;
                return (int)(Toolkit.getPointRelativeToMap(secondLane).Y - Toolkit.getPointRelativeToMap(firstLane).Y);
            }
        }

        /**
         * Comparer used to sort lanes by smallest Y-coordinate
         * */
        private class CompareLaneBySmallestY : IComparer
        {
            int IComparer.Compare(Object x, Object y)
            {
                if (!(x is Lane) || !(y is Lane)) throw new InvalidOperationException("Can only compare two lanes");
                Lane firstLane = (Lane)x, secondLane = (Lane)y;
                return (int)(Toolkit.getPointRelativeToMap(firstLane).Y - Toolkit.getPointRelativeToMap(secondLane).Y);
            }
        }
    }
}
